package cn.lingyangwl.agile.auth.service.impl;

import cn.lingyangwl.agile.auth.model.entity.*;
import cn.lingyangwl.agile.auth.model.rqrs.oauth2.*;
import cn.lingyangwl.agile.auth.oauth2.*;
import cn.lingyangwl.agile.auth.oauth2.model.*;
import cn.lingyangwl.agile.auth.oauth2.support.*;
import cn.lingyangwl.agile.auth.service.*;
import cn.lingyangwl.agile.auth.utils.*;
import cn.lingyangwl.agile.common.security.utils.*;
import cn.lingyangwl.agile.model.constants.*;
import cn.lingyangwl.agile.model.enums.*;
import cn.lingyangwl.agile.model.module.auth.*;
import cn.lingyangwl.agile.model.module.tenant.*;
import cn.lingyangwl.framework.tool.core.*;
import cn.lingyangwl.framework.tool.core.exception.*;
import cn.lingyangwl.framework.validation.*;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.impl.DefaultClaims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author shenguangyang
 */
@Slf4j
@Service
public class OAuth2TokenServiceImpl implements OAuth2TokenService {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private AuthHandler authHandler;
    @Resource
    private OAuth2ClientService clientService;

    @Override
    public LoginUser getLoginUser() {
        // 获取最新用户信息
        LoginUser currentLoginUser = SecurityUtils.getLoginUser();
        Object loginId = currentLoginUser.getLoginId();
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String key = String.format(OAuth2Cons.CACHE_USER_DETAILS, loginUser.getUserId(),loginId);
        return (LoginUser) redisTemplate.opsForValue().get(key);
    }

    @Override
    public void updateLoginUser(LoginUser loginUser) {
        if (Objects.isNull(loginUser)) {
            return;
        }
        LoginUser currentLoginUser = SecurityUtils.getLoginUser();
        Object loginId = currentLoginUser.getLoginId();
        String clientId = currentLoginUser.getClientId();
        OAuth2Client client = clientService.getByClientId(clientId);
        if (Objects.isNull(client)) {
            throw new BizException("当前授权客户端 [{}] 不存在, 请联系管理员", clientId);
        }

        String key = String.format(OAuth2Cons.CACHE_USER_DETAILS, loginUser.getUserId(), loginId);
        redisTemplate.opsForValue().set(key, loginUser, client.getAccessTokenValidity(), TimeUnit.SECONDS);
    }

    @Override
    public OAuth2TokenResp issueToken(OAuth2TokenContext tokenContext) {
        checkSameTimeOnline(tokenContext);
        saveUserLoginRecord(tokenContext);

        if (Objects.isNull(tokenContext.getRegisteredClient())) {
            throw new BizException(AuthErrorEnum.CLIENT_NOT_EXIST);
        }
        String refreshToken = createOAuth2RefreshToken(tokenContext);
        return createOAuth2AccessToken(refreshToken, tokenContext);
    }


    @Override
    public OAuth2TokenContext buildTokenContent(TokenContextBuildReq req) {
        ValidationUtils.validateFastThrow(req);
        // 设置Jwt公共的存储信息
        Map<String, Object> claims = Optional.ofNullable(req.getTokenClaims()).orElse(new HashMap<>());
        claims.put(TokenClaimsKeys.LOGIN_ID, req.getLoginId());
        claims.put(TokenClaimsKeys.OPEN_ID, req.getOpenId());
        claims.put(TokenClaimsKeys.USER_ID, req.getUserId());
        claims.put(TokenClaimsKeys.GRANT_TYPE, req.getGrantType().getCode());
        claims.put(TokenClaimsKeys.CLIENT_ID, req.getClient().getClientId());
        claims.put(TokenClaimsKeys.SCOPES, StringUtils.toStr(req.getClient().getScopes(), ","));

        LoginUser loginUser = new LoginUser();
        loginUser.setUserId(req.getUserId()).setLoginId(req.getLoginId()).setTerminal(req.getTerminal())
            .setClientId(req.getClient().getClientId()).setAppType(req.getClient().getAppType().getCode())
            .setLoginTime(System.currentTimeMillis()).setOpenId(req.getOpenId())
            .setGrantType(req.getGrantType().getCode()).setUserName(req.getLoginAccount())
            .setTenant(new AuthUserTenant()); // 当登录成功之后, 设置空租户, 如果想要使用功能, 必须调用切换到指定租户接口

        // 生成并保存token
        OAuth2TokenContext tokenContext = OAuth2TokenContext.builder()
            .authorizedScopes(req.getClient().getScopes()).registeredClient(req.getClient())
            .authUser(loginUser).claims(claims)
            .grantType(req.getGrantType().getCode()).loginId(req.getLoginId()).build();

        // 保存用户信息到缓存中
        Duration accessTokenValidity = req.getClient().getAccessTokenTimeToLive();

        // 保存用户基本信息
        String cacheKey = String.format(OAuth2Cons.CACHE_USER_DETAILS, req.getUserId(), req.getLoginId());
        log.info("user details cache key: {}", cacheKey);
        redisTemplate.opsForValue().set(cacheKey, loginUser, accessTokenValidity.getSeconds(), TimeUnit.SECONDS);
        return tokenContext;
    }

    /**
     * 检查用户同时在线
     */
    public void checkSameTimeOnline(OAuth2TokenContext tokenContext) {
        // 判断是否允许同时在线, 如果不允许, 则把登录的用户干掉
        RegisteredClient registeredClient = tokenContext.getRegisteredClient();
        LoginUser authUser = tokenContext.getAuthUser();
        boolean canSameTimeOnline = registeredClient.getCanSameTimeOnline();
        if (canSameTimeOnline && Objects.nonNull(authUser) && Objects.nonNull(authUser.getUserId())) {
            // 查找用户的登录记录
            String cacheKey = String.format(OAuth2Cons.CACHE_USER_LOGIN_RECORD, authUser.getUserId());
            // 删除前1000个登录记录
            Set<Object> preLoginRecordList = redisTemplate.opsForZSet().reverseRange(cacheKey, 0, 1000);
            if (CollectionUtils.isNotEmpty(preLoginRecordList)) {
                Set<String> keys = preLoginRecordList.stream().map(e -> LoginRecord.parseCacheValue(String.valueOf(e)))
                        .filter(Objects::nonNull)
                        .map(LoginRecord::genCacheValue)
                        .flatMap(loginId -> {
                            List<String> list = new ArrayList<>();
                            list.add(String.format(OAuth2Cons.CACHE_TOKEN, OAuth2ParamKeys.ACCESS_TOKEN, loginId));
                            list.add(String.format(OAuth2Cons.CACHE_TOKEN, OAuth2ParamKeys.REFRESH_TOKEN, loginId));
                            return list.stream();
                        })
                        .collect(Collectors.toSet());
                redisTemplate.delete(keys);
            }
        }
    }


    public void saveUserLoginRecord(OAuth2TokenContext tokenContext) {
        LoginUser authUser = tokenContext.getAuthUser();
        String loginId = tokenContext.getLoginId();
        RegisteredClient registeredClient = tokenContext.getRegisteredClient();
        if (Objects.isNull(authUser) || Objects.isNull(authUser.getUserId()) || StringUtils.isEmpty(loginId)) {
            return;
        }

        // 保存用户登录记录
        String cacheValue = new LoginRecord(
                authUser.getLoginTime(), loginId, registeredClient.getAccessTokenTimeToLive().getSeconds()
        ).genCacheValue();

        String cacheKey = String.format(OAuth2Cons.CACHE_USER_LOGIN_RECORD, authUser.getUserId());
        redisTemplate.opsForZSet().addIfAbsent(cacheKey, cacheValue, (double) System.currentTimeMillis() / 1000);

        // 删除前5个登录记录, 删除是否过期, 过期就删除
        Set<Object> preLoginRecordList = redisTemplate.opsForZSet().reverseRange(cacheKey, 0, 5);
        if (CollectionUtils.isNotEmpty(preLoginRecordList)) {
            List<String> expiredList = preLoginRecordList.stream().map(e -> LoginRecord.parseCacheValue(String.valueOf(e)))
                    .filter(Objects::nonNull).filter(LoginRecord::isExpired) // 判断是否过期
                    .map(LoginRecord::genCacheValue).collect(Collectors.toList());
            log.info("pre login record expired, userId: {}, size: {}",authUser.getUserId(), expiredList.size());
            if (CollectionUtils.isNotEmpty(expiredList)) {
                redisTemplate.opsForZSet().remove(cacheKey, expiredList);
            }
        }
    }

    public String createOAuth2RefreshToken(OAuth2TokenContext tokenContext) {
        tokenContext.getClaims().put(TokenClaimsKeys.TOKEN_TYPE, OAuth2Cons.TokenType.REFRESH_TOKEN);
        tokenContext.getClaims().put(TokenClaimsKeys.ISSUED_AT, System.currentTimeMillis());

        String tokenKey = OAuth2Cons.CACHE_TOKEN;
        String refreshToken = JwtUtils.createToken(tokenContext.getClaims(), tokenContext.getLoginId());

        Duration timeToLive = tokenContext.getRegisteredClient().getRefreshTokenTimeToLive();

        // 设置刷新令牌的过期时间为访问令牌的二倍
        Instant expiresAt = Instant.now().plus(timeToLive.toMillis() * 2, ChronoUnit.MINUTES);
        long between = ChronoUnit.SECONDS.between(Instant.now(), expiresAt);
        redisTemplate.opsForValue().set(String.format(tokenKey, OAuth2ParamKeys.REFRESH_TOKEN, tokenContext.getLoginId()),
                "1", between, TimeUnit.SECONDS);
        return refreshToken;
    }

    public OAuth2TokenResp createOAuth2AccessToken(String refreshToken, OAuth2TokenContext tokenContext) {
        // 标记为访问令牌, 和刷新令牌进行区分, 避免用户使用刷新令牌访问系统
        tokenContext.getClaims().put(TokenClaimsKeys.TOKEN_TYPE, OAuth2Cons.TokenType.ACCESS_TOKEN);
        tokenContext.getClaims().put(TokenClaimsKeys.ISSUED_AT, System.currentTimeMillis());

        String accessToken = JwtUtils.createToken(tokenContext.getClaims(), tokenContext.getLoginId());

        Duration timeToLive = tokenContext.getRegisteredClient().getAccessTokenTimeToLive();
        String clientId = tokenContext.getRegisteredClient().getClientId();

        Instant expiresAt = Instant.now().plus(timeToLive.toMillis(), ChronoUnit.MILLIS);
        long between = ChronoUnit.SECONDS.between(Instant.now(), expiresAt);

        OAuth2TokenResp tokenResp = OAuth2TokenResp.builder()
                .clientId(clientId).tokenType(OAuth2Cons.TokenType.BEARER)
                .scopes(new ArrayList<>(tokenContext.getAuthorizedScopes()))
                .loginId(tokenContext.getLoginId()).loginTenantId(TenantContext.get().getMainTenantId())
                .refreshToken(refreshToken)
                .issuedAt(LocalDateTime.ofInstant(Instant.now(), ZoneId.systemDefault()))
                .expiresAt(between)
                .userId(Optional.ofNullable(tokenContext.getAuthUser()).map(LoginUser::getUserId).orElse(null))
                .build();

        String cacheKey = String.format(OAuth2Cons.CACHE_TOKEN, OAuth2ParamKeys.ACCESS_TOKEN, tokenContext.getLoginId());
        redisTemplate.opsForValue().set(cacheKey, tokenResp, between, TimeUnit.SECONDS);

        // 生成的访问令牌(jwt)相对占用空间, 这里不存入到缓存中
        tokenResp.setAccessToken(accessToken);
        tokenResp.setOpenId((String) tokenContext.getClaims().get(TokenClaimsKeys.OPEN_ID));
        tokenResp.setGrantType((String) tokenContext.getClaims().get(TokenClaimsKeys.GRANT_TYPE));
        return tokenResp;
    }

    @Override
    public OAuth2TokenResp checkAccessToken(String accessToken) {
        Claims claims = JwtUtils.parseToken(accessToken);
        if (Objects.isNull(claims)) {
            throw new BizException(AuthErrorEnum.TOKEN_EXPIRED);
        }

        return buildOAuth2Token(accessToken, claims);
    }

    @Override
    public OAuth2TokenResp parseAccessToken(String accessToken) {
        Claims claims = JwtUtils.parseToken(accessToken);
        if (Objects.isNull(claims)) {
            return null;
        }

        return buildOAuth2Token(accessToken, claims);
    }

    private OAuth2TokenResp buildOAuth2Token(String accessToken, Claims claims) {
        // 从redis获取
        String loginId = claims.get(TokenClaimsKeys.LOGIN_ID, String.class);
        String openId = claims.get(TokenClaimsKeys.OPEN_ID, String.class);
        String grantType = claims.get(TokenClaimsKeys.GRANT_TYPE, String.class);
        String cacheKey = String.format(OAuth2Cons.CACHE_TOKEN, OAuth2ParamKeys.ACCESS_TOKEN, loginId);
        OAuth2TokenResp cacheValue = (OAuth2TokenResp) redisTemplate.opsForValue().get(cacheKey);
        if (Objects.isNull(cacheValue)) {
            throw new BizException(AuthErrorEnum.TOKEN_EXPIRED);
        }

        // 补充其他参数
        cacheValue.setAccessToken(accessToken);
        cacheValue.setOpenId(openId);
        cacheValue.setGrantType(grantType);
        return cacheValue;
    }


    @Override
    public boolean removeAccessToken(String accessToken) {
        Claims claims = JwtUtils.parseToken(accessToken);

        // 令牌可能已失效无需移除
        if (Objects.isNull(claims)) {
            log.warn("令牌失效, 退出流程中止");
            return false;
        }
        String loginId = claims.get(TokenClaimsKeys.LOGIN_ID, String.class);
        Long userId = claims.get(TokenClaimsKeys.USER_ID, Long.class);

        // 删除授权信息和用户信息
        String userKey = String.format(OAuth2Cons.CACHE_USER_DETAILS, userId, loginId);
        Set<String> deleteKeys = getAuthUserCacheKeys(userId, loginId);

        // 获取用户信息
        LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get(userKey);

        redisTemplate.delete(deleteKeys);

        if (Objects.nonNull(loginUser)) {
            authHandler.onLogout(loginUser);
        }
        return true;
    }

    /**
     * @param userId 用户id
     */
    @Override
    public boolean removeAccessTokenByUserId(Long userId) {
        // 查询登录记录
        String cacheKey = String.format(OAuth2Cons.CACHE_USER_LOGIN_RECORD, userId);

        Long size = redisTemplate.opsForZSet().size(cacheKey);
        if (Objects.isNull(size)) {
            return true;
        }
        Set<Object> loginRecord = Optional.ofNullable(redisTemplate.opsForZSet().range(cacheKey, 0, size)).orElse(Collections.emptySet());

        // 用户访问令牌key
        List<String> keys = loginRecord.stream().map(e -> LoginRecord.parseCacheValue(String.valueOf(e)))
                .filter(Objects::nonNull)
                .map(LoginRecord::getLoginId)
                .flatMap(loginId -> getAuthUserCacheKeys(userId, loginId).stream())
                .collect(Collectors.toList());

        log.info("delete user key, userId: {}, size: {}", userId, keys.size());

        redisTemplate.delete(keys);
        return true;
    }

    @Override
    public OAuth2TokenResp refreshAccessToken(String refreshToken) {
        // 解密访问令牌
        DefaultClaims claims = (DefaultClaims) JwtUtils.parseToken(refreshToken);
        if (Objects.isNull(claims)) {
            throw new BizException("无效的刷新令牌");
        }

        String loginId = claims.get(TokenClaimsKeys.LOGIN_ID, String.class);
        String clientId = claims.get(TokenClaimsKeys.CLIENT_ID, String.class);
        Long userId = claims.get(TokenClaimsKeys.USER_ID, Long.class);
        String grantType = claims.get(TokenClaimsKeys.GRANT_TYPE, String.class);
        String scopes = claims.get(TokenClaimsKeys.SCOPES, String.class);
        String tokenType = claims.get(TokenClaimsKeys.TOKEN_TYPE, String.class);

        if (!OAuth2Cons.TokenType.REFRESH_TOKEN.equals(tokenType)) {
            throw new BizException("请传入合法的刷新令牌");
        }

        // 由于jwt不能手动让其失效, 所以这里需要再次从缓存中校验访问令牌有效期
        // 那为什么不直接采用普通字符串作为jwt访问令牌呢？因为刷新令牌采用jwt可以避免一些安全问题, 避免可能产生他人盗刷刷新令牌
        String refreshTokenKey = String.format(OAuth2Cons.CACHE_TOKEN, OAuth2ParamKeys.REFRESH_TOKEN, loginId);
        Object refreshTokenCache = redisTemplate.opsForValue().get(refreshTokenKey);
        if (Objects.isNull(refreshTokenCache)) {
            throw new BizException("无效的刷新令牌");
        }

        // 校验 Client 匹配
        RegisteredClient client = clientService.validOAuthClient(clientId, null, null, null, null);
        if (Objects.isNull(client)) {
            throw new BizException("刷新令牌的客户端编号不正确");
        }

        // 从缓存中查找用户信息, 用户信息和 token在登录的时候一起存入到缓存中
        String userKey = String.format(OAuth2Cons.CACHE_USER_DETAILS, userId, loginId);
        LoginUser authUser = (LoginUser) redisTemplate.opsForValue().get(userKey);
        if (Objects.isNull(authUser)) {
            // 如果弹出该错误, 目前要求用户重新登录
            throw new BizException(AuthErrorEnum.TOKEN_EXPIRED);
        }

        OAuth2TokenContext tokenContext = OAuth2TokenContext.builder()
                .authorizedScopes(StringUtils.toList(scopes, ",", String.class))
                .registeredClient(client)
                .authUser(authUser).claims(claims)
                .grantType(grantType).loginId(loginId).build();

        // 创建访问令牌
        return createOAuth2AccessToken(refreshToken, tokenContext);
    }

    /**
     * 获取授权用户相关的缓存keys
     * @param userId 用户id
     * @param loginId 登录id
     */
    public Set<String> getAuthUserCacheKeys(Object userId, String loginId) {
        String accessTokenKey = String.format(OAuth2Cons.CACHE_TOKEN, OAuth2ParamKeys.ACCESS_TOKEN, loginId);
        String refreshTokenKey = String.format(OAuth2Cons.CACHE_TOKEN, OAuth2ParamKeys.REFRESH_TOKEN, loginId);
        String userKey = String.format(OAuth2Cons.CACHE_USER_DETAILS, userId, loginId);

        Set<String> keys = new HashSet<>();
        keys.add(accessTokenKey);
        keys.add(refreshTokenKey);
        keys.add(userKey);
        return keys;
    }

}
